// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using SnapObjects.Commons;

namespace SnapObjects.Data
{
    internal abstract class ModelTracker<TModel> : IModelTracker
    {
        protected readonly IMapperContext _mapperContext;

        protected readonly TModel _model;
        protected readonly UpdatableBuilder _updateBuilder;

        protected readonly Action<ISaveContext> _beforeSaveAction;
        protected readonly Action<ISaveContext> _afterSaveAction;

        protected IStatefulModel _statefulModel;

        public ModelTracker(
            IMapperContext mapperContext,
            TModel model,
            Action<ISaveContext> beforeSaveAction,
            Action<ISaveContext> afterSaveAction)
        {
            _mapperContext = mapperContext;
            _model = model;
            _updateBuilder = new UpdatableBuilder(mapperContext);

            _beforeSaveAction = beforeSaveAction;
            _afterSaveAction = afterSaveAction;
        }

        /// <summary>
        ///     Occurs when the current value is changed.
        /// </summary>
        public event Action<string, object> CurrentValueChanged;

        public TrackerType TrackerType => TrackerType.Model;

        /// <summary>
        ///     Gets the tracked state of the data.
        /// </summary>
        public ModelState ModelState => this.StatefulModel.LatestModelState;

        private PocoModelMeta ModelMeta => ModelMetaManager.GetModelMeta(typeof(TModel));

        /// <summary>
        ///     Gets the property names of the model class.
        /// </summary>
        public string[] Properties => this.ModelMeta.Properties.Select(o => o.Name).ToArray();

        /// <summary>
        ///     Gets the tracked state of the specified property.
        /// </summary>
        /// <param name="name">The name of the property.</param>
        /// <returns>The tracked state of the specified property.</returns>
        public PropertyState GetPropertyState(string name)
        {
            return this.StatefulModel.GetPropertyState(this.ModelMeta.IndexOf(name));
        }

        /// <summary>
        ///     Gets the original value of the specified property.
        /// </summary>
        /// <param name="name">The name of the property.</param>
        /// <returns>The original value of the specified property.</returns>
        public object GetOriginalValue(string name)
        {
            return this.StatefulModel.GetOriginalValue(name);
        }

        /// <summary>
        ///     Gets the current value of the specified property.
        /// </summary>
        /// <param name="name">The name of the property.</param>
        /// <returns>The current value of the specified property.</returns>
        public object GetCurrentValue(string name)
        {
            return this.StatefulModel.GetCurrentValue(name);
        }

        protected virtual IStatefulModel CreateStatefulModel(TModel model, ModelState modelState,
            ChangeTrackingStrategy changeTrackingStrategy = ChangeTrackingStrategy.Snapshot)
        {
            var compositeModelAccessor = _mapperContext.GetCompositeModelAccessor(typeof(TModel));

            _statefulModel = PocoStatefulModelFactory.Create(typeof(TModel),
                                    compositeModelAccessor, model, modelState, changeTrackingStrategy);

            return _statefulModel;
        }

        public IReadOnlyList<IUpdatableModel> GetUpdateableModels()
        {
            _updateBuilder.Reset();
            this.BuildUpdateableModels(_model);

            return _updateBuilder.UpdatableModels;
        }

        public IUpdatableModel GetMasterModel()
        {
            var masterModelMeta = ModelMetaManager.GetModelMeta(_model.GetType());

            return new UpdatableModel<TModel>(masterModelMeta,
                            ChangeTrackingStrategy.Snapshot,
                            masterModelMeta.UpdateWhereStrategy,
                            _statefulModel,
                            true,
                            _beforeSaveAction,
                            _afterSaveAction);
        }

        public virtual IList<ValidationResult> Validate()
        {
            var validationResults = new List<ValidationResult>();

            this.Validate(_model, validationResults);

            return validationResults;
        }

        private void Validate(object model, List<ValidationResult> validationResults)
        {
            var composite = _mapperContext.GetCompositeModelAccessor(model.GetType());
            var validator = composite.NonGenericModelFactory.GetValidator();
            var result = validator.IsValid(model, out var tempResults);
            var accessor = composite.NonGenericModelFactory.GetAccessor();
            

            if (tempResults.Count > 0)
            {
                validationResults.AddRange(tempResults);
            }

            //Since cascading updates are not supported, this no need to check for embedded properties.
            if (_statefulModel.LatestModelState == ModelState.Modified)
            {
                return;
            }

            if (accessor.ModelMeta is IPocoModelMeta modelMeta)
            {
                var embeddedProperties = modelMeta.EmbeddedProperties;

                foreach (var property in embeddedProperties)
                {
                    if (property.EmbeddedType.IsModel())
                    {
                        var embeddedFactory = composite.GetEmbeddedModelFactory(property.Name);
                        var embeddedValid = embeddedFactory.GetValidator();
                        var propertyValue = accessor.GetValue(model, property.Name);

                        if (propertyValue != null)
                        {
                            if (propertyValue is IEnumerable embedded)
                            {
                                foreach (var pitem in embedded)
                                {
                                    this.Validate(pitem, validationResults);
                                }
                            }
                            else
                            {
                                this.Validate(propertyValue, validationResults);
                            }
                        }
                    }
                }
            }
        }

        protected abstract IStatefulModel StatefulModel { get; }

        protected abstract void BuildUpdateableModels(object model);

        /// <summary>
        ///     Sets the current value for the specified property.
        /// </summary>
        /// <param name="name">The name of the property.</param>
        /// <param name="value">The value set to the specified property.</param>
        public void SetCurrentValue(string name, object value)
        {
            this.CurrentValueChanged(name, value);

            _statefulModel.SetCurrentValue(name, value);
        }
    }
}
